-------------------------------------------------------------------------------- -- Psi Storm manager class ----------------------------------------------------- -- Made by Cromm Cruac --------------------------------------------------------- -- for AtmosFear 3 ------------------------------------------------------------- -- 25.06.2011 ------------------------------------------------------------------ -------------------------------------------------------------------------------- -- Modified by Alundaio -- (see surge_manager.script) -- removed duplicate functions also used by surge_manager --[[ Tronex 2019/9/13 Modified for Anomaly 1.5 --]] -- class instance PsiStormManager = nil function get_psi_storm_manager() PsiStormManager = PsiStormManager or CPsiStormManager() return PsiStormManager end --------------------------------------------------------------------- local surge_time_factor = 10 local prev_sec = -1 local prev_game_sec = -1 local diff_sec = -1 local diff_game_sec = -1 class "CPsiStormManager" function CPsiStormManager:__init() self.humanParticles = {} self.vortexes = {} end function CPsiStormManager:initialize() self.ini = surge_manager.SurgeManager.ini if not (self._state_loaded) then self.game_time_factor=level.get_time_factor() self.last_psi_storm_time = game.get_game_time() self.inited_time = game.get_game_time() self.started = false self.finished = true self.task_given = nil end if not (self._delta) then local freq = ui_options.get("alife/event/psi_storm_frequency") self._delta = math.random(math.floor(freq/2)*3600, freq*3600) end self.vortex_index=0 self.vortex_period_min=8 self.vortex_period_max=16 self.vortex_distance_max=150 self.next_vortex_at=nil self.max_distance_psi_hit=200 self.max_distance_electro_hit=50 self.max_distance_npc_hit=50 self.psi_storm_duration = 254 -- (duration: about 40mins of game time) self.psi_storm_message = "" self.psi_storm_task_sect = "" self.time_forwarded = false end function CPsiStormManager:start(manual) if not (self._state_loaded) then self:initialize() end self.game_time_factor=level.get_time_factor() local Y, M, D, h, m, s, ms = 0, 0, 0, 0, 0, 0, 0 Y, M, D, h, m, s, ms = self.last_psi_storm_time:get(Y, M, D, h, m, s, ms) if(manual or not self.inited_time) then self.inited_time = game.get_game_time() else self.inited_time:set(Y, M, D, h, m, s + self._delta, ms) end diff_sec = math.ceil(game.get_game_time():diffSec(self.inited_time)/level.get_time_factor()) local level_name = level.name() local flags = {allow = true} SendScriptCallback("on_before_psi_storm",flags) if( (level_weathers.valid_levels[level_name] ~= true) or (not ui_options.get("alife/event/psi_storm_state")) or flags.allow == false) then self:skip_psi_storm() return end -- Generators is psi-storm source and Deserted Hospital has nowhere to hide (almost all the map is inside anyway). if (level_name == "l13_generators" or level_name == "l11_hospital" or has_alife_info("bar_arena_fight")) then self.skip_message = true self:skip_psi_storm() return end if(diff_sec+6>self.psi_storm_duration) then self.skip_message = true self:skip_psi_storm() else self.started = true self.finished = false self.hitFactor=0 self.stages = empty_table(self.stages) self.vortexes = empty_table(self.vortexes) for k,v in pairs(self.humanParticles) do if (v:playing()) then v:stop() end end self.humanParticles = empty_table(self.humanParticles) self.next_vortex_at = nil level.set_time_factor(surge_time_factor) self.task_given = nil SendScriptCallback("actor_on_interaction", "anomalies", nil, "psi_storm_start") end end function CPsiStormManager:new_psi_storm_time() self.last_psi_storm_time = game.get_game_time() local freq = ui_options.get("alife/event/psi_storm_frequency") self._delta = math.random(math.floor(freq/2)*3600, freq*3600) local SM = surge_manager.get_surge_manager() local last_surge_time = SM and SM.last_surge_time or game.get_game_time() local g_time = game.get_game_time() local psi_storm_start = math.floor(self._delta - g_time:diffSec(self.last_psi_storm_time)) local psi_storm_end = math.floor(self._delta+3600 - g_time:diffSec(self.last_psi_storm_time)) local surge_start = math.floor(SM._delta - g_time:diffSec(last_surge_time)) local surge_end = math.floor(SM._delta+3600 - g_time:diffSec(last_surge_time)) if ((psi_storm_end > surge_start) and (psi_storm_end < surge_end)) then --1h earlier self._delta=self._delta-3600 end if ((psi_storm_start > surge_start) and (psi_storm_start < surge_end)) then --1h later self._delta=self._delta+3600 end end function CPsiStormManager:skip_psi_storm() if not (self.inited_time) then self.inited_time = game.get_game_time() end if not (self.last_psi_storm_time) then self.last_psi_storm_time = game.get_game_time() end local Y, M, D, h, m, s, ms = 0, 0, 0, 0, 0, 0, 0 Y, M, D, h, m, s, ms = self.inited_time:get(Y, M, D, h, m, s, ms) self.last_psi_storm_time:set(Y, M, D, h, m, s + self.psi_storm_duration, ms) self:new_psi_storm_time(false) self.started = false self.finished = true self.psi_storm_message = "" self.psi_storm_task_sect = "" self.task_given = nil prev_sec = 0 --game_statistics.increment_statistic("psi_storms") if (self.skip_message and ui_options.get("alife/event/psi_storm_state")) then news_manager.send_tip(db.actor, "st_surge_while_asleep", nil, "recent_surge", nil, nil) game_statistics.increment_statistic("psi_storms") self.skip_message = nil SendScriptCallback("actor_on_interaction", "anomalies", nil, "psi_storms") SendScriptCallback("actor_on_interaction", "anomalies", nil, "psi_storm_end") --release_body_manager.get_release_body_manager():clear(true) end level.set_time_factor(self.game_time_factor) end function CPsiStormManager:finalize() for k,v in pairs(self.vortexes) do if (v.effect and v.effect:playing()) then v.effect:stop() end end for k,effect in pairs(self.humanParticles) do if (effect:playing()) then effect:stop() end end end function CPsiStormManager:finish(manual) if (self.started) then game_statistics.increment_statistic("psi_storms") SendScriptCallback("actor_on_interaction", "anomalies", nil, "psi_storms") SendScriptCallback("actor_on_interaction", "anomalies", nil, "psi_storm_end") end self.started = false self.finished = true self.last_psi_storm_time = game.get_game_time() self:new_psi_storm_time(false) for k,v in pairs(db.signal_light) do v:stop_light() v:stop() end self.psi_storm_message = "" self.psi_storm_task_sect = "" self.task_given = nil if(manual or (self.time_forwarded and level_weathers.get_weather_manager().weather_fx)) then for key,vortex in pairs(self.vortexes) do vortex.effect:stop() vortex.sound:stop() end level.stop_weather_fx() level_weathers.get_weather_manager():forced_weather_change() end for k,snd in pairs(surge_manager.SurgeManager.blowout_sounds) do if snd ~= nil and snd:playing() then snd:stop() end end prev_sec = 0 for k,v in pairs(self.humanParticles) do if (v:playing()) then v:stop() end end self.humanParticles = empty_table(self.humanParticles) self.vortexes = empty_table(self.vortexes) self.stages = empty_table(self.stages) level.set_time_factor(self.game_time_factor) end ---------------------------------- -- Update ---------------------------------- function CPsiStormManager:update() if not (self._state_loaded) then self:initialize() return end if not (self.first_update) then self.first_update = true local level_name = level.name() local flags = {allow = true} SendScriptCallback("on_before_psi_storm",flags) if (level_name == "l13_generators" or level_name == "l11_hospital" or has_alife_info("bar_arena_fight") or flags.allow == false or level_weathers.valid_levels[level_name] ~= true) then if (self.started) then self:finish() else self:skip_psi_storm() end return end local diff_sec = math.ceil(game.get_game_time():diffSec(self.inited_time)/level.get_time_factor()) if (diff_sec + 6 > self.psi_storm_duration) then self:skip_psi_storm() return end end SetEvent("psi_storm", "state", self.started) if not (self.started) then local g_time = game.get_game_time() if(self.time_forwarded) then local diff = math.abs(self._delta - g_time:diffSec(self.last_psi_storm_time)) if(diff<3600) then self._delta = 3*3600+g_time:diffSec(self.last_psi_storm_time) end self.time_forwarded = false end if(g_time:diffSec(self.last_psi_storm_time) < self._delta) then return end self:start() return elseif (surge_manager.is_started()) then self:skip_psi_storm() end diff_sec = math.ceil(game.get_game_time():diffSec(self.inited_time)/level.get_time_factor()) if (diff_sec >= self.psi_storm_duration) then self:finish() return end local actor_pos=db.actor:position() if(prev_sec~=diff_sec) then prev_sec = diff_sec SetEvent("psi_storm", "time", diff_sec) if (not ui_options.get("alife/event/psi_storm_state")) then self:finish() return end -- psi storm begins ----------------------------------------------------------------------------------------------------------------------------------------- if (diff_sec>=0) and (self.stages['beginning']==nil) then if (level.get_time_hours() >= 5 and level.get_time_hours() <= 20) then level.set_weather_fx("fx_psi_storm_day") else level.set_weather_fx("fx_psi_storm_night") end self.stages['beginning']=true end -- siren warning ----------------------------------------------------------------------------------------------------------------------------------------- if (diff_sec>=10) and (self.stages['siren']==nil) and (ui_options.get("alife/event/psi_storm_warning")=="siren") then self:launch_rockets() surge_manager.SurgeManager:play_siren_sound() self.stages['siren']=true end -- psi storm warning ----------------------------------------------------------------------------------------------------------------------------------------- if (diff_sec>=12) and not(self.task_given) then if (ui_options.get("alife/event/psi_storm_task")) then self:give_psi_storm_hide_task() end self.task_given = true end -- psi storm main phase ----------------------------------------------------------------------------------------------------------------------------------------- if (diff_sec>=40) and (diff_sec<=228) then if self.next_vortex_at == nil then self.next_vortex_at = diff_sec + math.random(self.vortex_period_min, self.vortex_period_max) end if self.next_vortex_at == diff_sec then self:vortex() self.next_vortex_at = diff_sec + math.random(self.vortex_period_min, self.vortex_period_max) end end for key,vortex in pairs(self.vortexes) do local life_time = diff_sec-vortex.start_time if life_time>=18 and vortex.hit==false then SetEvent("psi_storm", "vortex", vortex.sound_pos) end if life_time>=20 and vortex.hit==false then self:kill_crows_at_pos(vortex.sound_pos) local fate = ui_options.get("alife/event/psi_storm_fate") if (fate ~= "unhurt") then self:kill_objects_at_pos(vortex.sound_pos, fate) end if (not GetEvent("current_safe_cover")) then self:vortex_actor_hit(vortex) end vortex.hit=true end if life_time>=23 then SetEvent("psi_storm", "vortex", false) end if life_time>=25 then vortex=nil end end end end ---------------------------------- -- Effects ---------------------------------- function CPsiStormManager:vortex() self.vortex_index=self.vortex_index+1 local effect=particles_object("crommcruac\\psi_storm_01") local sound=sound_object("anomaly\\psi_storm_01") local actor_pos=db.actor:position() local angle_dec=math.random(0,359) local angle_rad=math.rad(angle_dec) local distance=math.random(0,self.vortex_distance_max) local pos_x=math.cos(angle_rad)*distance local pos_z=math.sin(angle_rad)*distance local particle_pos= vector():set(actor_pos.x+pos_x, actor_pos.y+20, actor_pos.z+pos_z) local sound_pos= vector():set(actor_pos.x+pos_x, actor_pos.y+60, actor_pos.z+pos_z) self.vortexes[self.vortex_index]={effect=effect, particle_pos=particle_pos,sound=sound, sound_pos=sound_pos, start_time=diff_sec, hit=false} --particle self.vortexes[self.vortex_index].effect:play_at_pos(self.vortexes[self.vortex_index].particle_pos) --sound if self.vortexes[self.vortex_index].sound ~= nil and self.vortexes[self.vortex_index].sound:playing() then self.vortexes[self.vortex_index].sound:stop() end if self.vortexes[self.vortex_index].sound ~= nil then self.vortexes[self.vortex_index].sound:play_at_pos(db.actor, self.vortexes[self.vortex_index].sound_pos) self.vortexes[self.vortex_index].sound.volume = 1 end end function CPsiStormManager:launch_rockets() for k,v in pairs(db.signal_light) do if not(v:is_flying()) then v:launch() end end end ---------------------------------- -- NPC fate ---------------------------------- function CPsiStormManager:vortex_actor_hit(vortex) local hit_power=0 local distance = distance_2d(db.actor:position(), vortex.particle_pos) if distance < self.max_distance_psi_hit then hit_power=math.cos(distance * math.pi / self.max_distance_psi_hit)+1 local h = hit() h.type = hit.telepatic if (level_environment.is_actor_immune() or dialogs_yantar.actor_has_psi_helmet()) then h.power = 0 else h.power = surge_manager.SurgeManager:hit_power(hit_power, h.type) end h.impulse = 0 h.direction = VEC_Z h.draftsman = db.actor db.actor:hit(h) level.remove_pp_effector(666) level.add_pp_effector("psi_fade.ppe", 666, false) level.set_pp_effector_factor(666,h.power) end if distance < self.max_distance_electro_hit then hit_power=math.cos(distance * math.pi / self.max_distance_electro_hit)+1 local h = hit() h.type = hit.shock if (level_environment.is_actor_immune()) then h.power = 0 else h.power = surge_manager.SurgeManager:hit_power(hit_power, h.type) end h.impulse = 0 h.direction = VEC_Z h.draftsman = db.actor db.actor:hit(h) level.remove_pp_effector(667) level.add_pp_effector("electro_fade.ppe", 667, false) level.set_pp_effector_factor(667,h.power) end end function CPsiStormManager:kill_objects_at_pos(vortex_pos, fate) if (load_var(db.actor,"surge_immuned",false) == true) then return end if (not vortex_pos) then return end --printdbg("Psi-Storm Manager | kill_objects_at_pos | pos: %s - fate: %s", vortex_pos, fate) local sm = surge_manager.get_surge_manager() local id, comm, npc_pos, se_npc, npc, squad, squad_id, smart, surge_smart local board = SIMBOARD local sim = alife() for i=1, #db.OnlineStalkers do id = db.OnlineStalkers[i] npc = db.storage[id] and db.storage[id].object or level.object_by_id(id) if npc then comm = npc:character_community() npc_pos = npc:position() if npc_pos and IsStalker(npc) -- must be stalker and npc:alive() -- must be alive and (comm ~= "monolith") -- not monolith (immune) and (comm ~= "zombied") -- not zombied (immune) and (not get_object_story_id(id)) -- not story npc and (distance_2d(vortex_pos, npc_pos) < self.max_distance_npc_hit) -- npc is close to psi-vortex then se_npc = sim:object(id) squad_id = se_npc.group_id squad = squad_id and sim:object(squad_id) if se_npc and squad then smart = board and squad.smart_id and board.smarts[squad.smart_id] and board.smarts[squad.smart_id].smrt surge_smart = smart and smart.props and (tonumber(smart.props["surge"]) > 0) if not (surge_smart or sm:pos_in_cover(npc_pos)) then -- if stalker is not in a safe smart or not inside self.humanParticles[squad_id.."_"..id] = particles_object("crommcruac\\electrocute_human") self.humanParticles[squad_id.."_"..id]:play_at_pos(npc_pos) if (fate == "turn_to_zombie") then sm:turn_to_zombie(se_npc,squad) elseif (fate == "explode") then sm:explode(se_npc,squad) else npc:kill(npc) end end end end end end end function CPsiStormManager:kill_crows_at_pos(pos) --self.crowParticles={} local h = hit() h.type = hit.fire_wound h.power = 1.0 h.impulse = 0 h.direction = VEC_Z h.draftsman = db.actor for k,v in pairs(bind_crow.crow_storage) do local obj = alife_object(v) if obj then local crow = level.object_by_id(obj.id) if(crow and crow:alive() and distance_2d(pos, crow:position())<25) then --crow:start_particles("crommcruac\\electrocute_crow","bip01_pelvis") --self.crowParticles[k]=particles_object("crommcruac\\electrocute_crow") --self.crowParticles[k]:play_at_pos(crow:position()) crow:hit(h) end end end end ---------------------------------- -- Task ---------------------------------- function CPsiStormManager:give_psi_storm_hide_task() if(self.psi_storm_message~="empty") then local mess = "" if(self.psi_storm_message=="") then local time = 0 mess = game.translate_string("hide_from_psi_storm_message") else mess = game.translate_string(self.psi_storm_message) end end if(self.psi_storm_task_sect~="empty") then if(self.psi_storm_task_sect=="") then task_manager.get_task_manager():give_task("hide_from_psi_storm") else task_manager.get_task_manager():give_task(self.psi_storm_task_sect) end end end function get_task_title() if GetEvent("current_safe_cover") then return "hide_from_psi_storm_name_2" end return "hide_from_psi_storm_name_1" end function get_task_descr() if GetEvent("current_safe_cover") then return game.translate_string("hide_from_psi_storm_descr_2_a") end return game.translate_string("hide_from_psi_storm_descr_1_a") end function get_task_target() return surge_manager.get_task_target() end ---------------------------------- -- Data management ---------------------------------- function save_state(m_data) --utils_data.debug_write("PsiStormManager:save_state BEFORE") m_data.PsiStormManager = {} local mgr = get_psi_storm_manager() if not (mgr._state_loaded) then mgr:initialize() mgr._state_loaded = true end m_data.PsiStormManager.finished = mgr.finished == nil and true or mgr.finished m_data.PsiStormManager.started = mgr.started == nil and false or mgr.started m_data.PsiStormManager.last_psi_storm_time = mgr.last_psi_storm_time and utils_data.CTime_to_table(mgr.last_psi_storm_time) or game.get_game_time() if (mgr.started) then m_data.PsiStormManager.inited_time = mgr.inited_time and utils_data.CTime_to_table(mgr.inited_time) or game.get_game_time() m_data.PsiStormManager.task_given = mgr.task_given m_data.PsiStormManager.game_time_factor = mgr.game_time_factor end m_data.PsiStormManager._delta = mgr._delta --utils_data.debug_write("PsiStormManager:save_state AFTER") end function load_state(m_data) if not (m_data.PsiStormManager) then return end --utils_data.debug_write("PsiStormManager:load_state BEFORE") local mgr = get_psi_storm_manager() mgr:initialize() mgr.finished = m_data.PsiStormManager.finished mgr.started = m_data.PsiStormManager.started mgr.last_psi_storm_time = m_data.PsiStormManager.last_psi_storm_time and utils_data.CTime_from_table(m_data.PsiStormManager.last_psi_storm_time) or game.get_game_time() if (mgr.started == true and mgr.finished == false) then mgr.inited_time = m_data.PsiStormManager.inited_time and utils_data.CTime_from_table(m_data.PsiStormManager.inited_time) or game.get_game_time() mgr.task_given = m_data.PsiStormManager.task_given or false mgr.game_time_factor = m_data.PsiStormManager.game_time_factor or level.get_time_factor() mgr.stages = empty_table(mgr.stages) for k,v in pairs(mgr.humanParticles) do if (v:playing()) then v:stop() end end mgr.humanParticles = empty_table(mgr.humanParticles) mgr.vortexes = empty_table(mgr.vortexes) mgr.stages = empty_table(mgr.stages) else mgr.started = false mgr.finished = true end mgr._delta = m_data.PsiStormManager._delta mgr._state_loaded = true m_data.PsiStormManager = nil --utils_data.debug_write("PsiStormManager:save_state AFTER") end function actor_on_save(binder,packet) if (USE_MARSHAL) then return end -- load defaults if not loaded local mgr = get_psi_storm_manager() if not (mgr._state_loaded) then mgr:initialize() mgr._state_loaded = true end set_save_marker(packet, "save", false, "PsiStorm") packet:w_bool(mgr.finished) packet:w_bool(mgr.started) utils_data.w_CTime(packet, mgr.last_psi_storm_time) if(mgr.started) then utils_data.w_CTime(packet, mgr.inited_time) packet:w_bool(mgr.task_given) packet:w_u32(mgr.game_time_factor) end packet:w_u32(mgr._delta) set_save_marker(packet, "save", true, "PsiStorm") end function actor_on_load(binder,packet) local mgr = get_psi_storm_manager() if not (mgr._state_loaded) then mgr:initialize() mgr._state_loaded = true end if (USE_MARSHAL) then return end set_save_marker(packet, "load", false, "PsiStorm") mgr.finished = packet:r_bool() mgr.started = packet:r_bool() mgr.last_psi_storm_time = utils_data.r_CTime(packet) if(mgr.started) then mgr.inited_time = utils_data.r_CTime(packet) mgr.task_given = packet:r_bool() mgr.game_time_factor = packet:r_u32() mgr.stages = empty_table(mgr.stages) for k,v in pairs(mgr.humanParticles) do if (v:playing()) then v:stop() end end mgr.humanParticles = empty_table(mgr.humanParticles) mgr.vortexes = empty_table(mgr.vortexes) mgr.stages = empty_table(mgr.stages) end mgr._delta = packet:r_u32() set_save_marker(packet, "load", true, "PsiStorm") end ---------------------------------- -- Utilities ---------------------------------- function start_psi_storm(p) if get_psi_storm_manager() then get_psi_storm_manager():start(true) end end function stop_psi_storm(p) if get_psi_storm_manager() then get_psi_storm_manager():finish(true) end end function is_started() return get_psi_storm_manager().started end function is_finished() return get_psi_storm_manager() == nil and true or get_psi_storm_manager().finished == true end function is_loaded() return PsiStormManager and PsiStormManager._state_loaded == true end function distance_3d(p1, p2) --return math.sqrt( (b.x-a.x)^2 + (b.z-a.z)^2 ) return p1:distance_to(p2) end